﻿//-----------------------------------------------------------------------
// <copyright file="FeatureAuthorizationAttribute.cs" company="OOHM PROCESSAMENTO DE DADOS LTDA.">
//     Microsoft Public License (MS-PL) This license governs use of the accompanying
//     software. If you use the software, you accept this license. If you do not
//     accept the license, do not use the software. 1. Definitions The terms "reproduce,"
//     "reproduction," "derivative works," and "distribution" have the same meaning
//     here as under U.S. copyright law. A "contribution" is the original software,
//     or any additions or changes to the software. A "contributor" is any person
//     that distributes its contribution under this license. "Licensed patents"
//     are a contributor's patent claims that read directly on its contribution.
//     2. Grant of Rights (A) Copyright Grant- Subject to the terms of this license,
//     including the license conditions and limitations in section 3, each contributor
//     grants you a non-exclusive, worldwide, royalty-free copyright license to
//     reproduce its contribution, prepare derivative works of its contribution,
//     and distribute its contribution or any derivative works that you create.
//     (B) Patent Grant- Subject to the terms of this license, including the license
//     conditions and limitations in section 3, each contributor grants you a non-exclusive,
//     worldwide, royalty-free license under its licensed patents to make, have
//     made, use, sell, offer for sale, import, and/or otherwise dispose of its
//     contribution in the software or derivative works of the contribution in
//     the software. 3. Conditions and Limitations (A) No Trademark License- This
//     license does not grant you rights to use any contributors' name, logo, or
//     trademarks. (B) If you bring a patent claim against any contributor over
//     patents that you claim are infringed by the software, your patent license
//     from such contributor to the software ends automatically. (C) If you distribute
//     any portion of the software, you must retain all copyright, patent, trademark,
//     and attribution notices that are present in the software. (D) If you distribute
//     any portion of the software in source code form, you may do so only under
//     this license by including a complete copy of this license with your distribution.
//     If you distribute any portion of the software in compiled or object code
//     form, you may only do so under a license that complies with this license.
//     (E) The software is licensed "as-is." You bear the risk of using it. The
//     contributors give no express warranties, guarantees or conditions. You may
//     have additional consumer rights under your local laws which this license
//     cannot change. To the extent permitted under your local laws, the contributors
//     exclude the implied warranties of merchantability, fitness for a particular
// </copyright>
//-----------------------------------------------------------------------
namespace AbstractSaaSTemplate.UI.Web.Filters.Actions.Security{    using System;
    using System.Web;
    using System.Web.Mvc;
    using AbstractSaaSTemplate.Domain.Models.Enumerators;
    using AbstractSaaSTemplate.Domain.Services;

    /// <summary>
    /// Attempts to authorize the feature against the security definition
    /// </summary>
    [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, Inherited = true, AllowMultiple = false)]
    public sealed class FeatureAuthorizationAttribute : AuthorizeAttribute
    {
        /// <summary>
        /// Initializes a new instance of the FeatureAuthorizationAttribute class.
        /// </summary>
        /// <param name="levels">The access level bitwise related to the route marked with this feature</param>
        /// <param name="features">The features bitwise related to the route marked with this feature</param>
        public FeatureAuthorizationAttribute(
            AccessLevels levels,
            ControlledFeatures features) : this(levels, features, false)
        {
        }
        
        /// <summary>
        /// Initializes a new instance of the FeatureAuthorizationAttribute class.
        /// </summary>
        /// <param name="levels">The access level bitwise related to the route marked with this feature</param>
        /// <param name="features">The features bitwise related to the route marked with this feature</param>
        /// <param name="landingAction">The value indicating whether this attribute is marking the landing route for the feature.</param>
        public FeatureAuthorizationAttribute(
            AccessLevels levels,
            ControlledFeatures features,
            bool landingAction)
        {
            this.ServiceForMemberships = this.GetServiceFor<IMembershipService>();
            this.Levels = levels;
            this.Features = features;
            this.LandingAction = landingAction;
        }

        /// <summary>
        /// Gets the service to consume 'Memberships'.
        /// </summary>
        public IMembershipService ServiceForMemberships { get; private set; }

        /// <summary>
        /// Gets the access levels for the route marked with this attribute.
        /// </summary>
        public AccessLevels Levels { get; private set; }

        /// <summary>
        /// Gets the features for the route marked with this attribute.
        /// </summary>
        public ControlledFeatures Features { get; private set; }

        /// <summary>
        /// Gets a value indicating whether this attribute is marking the landing route for the feature.
        /// </summary>
        public bool LandingAction { get; private set; }

        /// <summary>
        /// Checks to see if the user is authenticated and has the
        /// correct role to access a particular view.
        /// </summary>
        /// <param name="httpContext">The http context</param>
        /// <returns>Returns true if authorized; false otherwized.</returns>
        protected override bool AuthorizeCore(HttpContextBase httpContext)
        {
            if (httpContext == null)
            {
                throw new ArgumentNullException("httpContext");
            }

            // Make sure the user is authenticated.
            if (!httpContext.User.Identity.IsAuthenticated)
            {
                return false;
            }

            // Test also if there is an instance of Active user just in case.
            if (this.ServiceForMemberships.ActiveUser == null)
            {
                return false;
            }

            // here we check if this user has enough access level for the feature
            var activeUser = this.ServiceForMemberships.ActiveUser;
            if ((activeUser.AccessLevel & this.Levels) != activeUser.AccessLevel)
            {
                return false;
            }

            // we will test all roles to see if the roles defined
            // as authorized are roles related to the current Principal.
            var systemRoles = Enum.GetNames(typeof(AccessRoles));
            foreach (var systemRole in systemRoles)
            {
                // we try to convert the role name
                // back to the enumeration
                var systemRoleToTest = AccessRoles.None;
                if (!Enum.TryParse<AccessRoles>(systemRole, out systemRoleToTest))
                {
                    continue;
                }

                // of course, we ignore the NotSet role.
                if (systemRoleToTest == AccessRoles.None)
                {
                    continue;
                }

                // before we start, we verify if the role
                // is one of the roles in which this member is
                // linked to. if not, we ignore the role
                ////if (!httpContext.User.IsInRole(systemRole))
                if (!this.ServiceForMemberships.IsActiveUserInRole(systemRoleToTest))
                {
                    continue;
                }

                // here we check access.
                var hasAccess = this.HasRoleAccessToArea(
                    systemRoleToTest,
                    this.Features);

                if (hasAccess)
                {
                    return true;
                }
            }

            return false;
        }

        /// <summary>
        /// Checkes and returns if any of the bitwised features are allowed for the role.
        /// </summary>
        /// <param name="role">The role for which we will check</param>
        /// <param name="features">The features bitwise for which we will check access.</param>
        /// <returns>If the role has access to the feature, returns true; otherwise returns false.</returns>
        private bool HasRoleAccessToArea(
            AccessRoles role,
            ControlledFeatures features)
        {
            // we lock the active user that will be tested
            var activeUser = this.ServiceForMemberships.ActiveUser;

            // scan through all the features to see if, for any of them,
            // the user has access.
            var controlledFeatures = Enum.GetNames(typeof(ControlledFeatures));
            foreach (var controlledFeature in controlledFeatures)
            {
                var featureToTest = ControlledFeatures.None;
                if (!Enum.TryParse<ControlledFeatures>(controlledFeature, out featureToTest))
                {
                    continue;
                }

                if ((features & featureToTest) != featureToTest)
                {
                    continue;
                }

                var hasAccess = this.ServiceForMemberships.HasRoleAccessToFeature(
                    activeUser.AccessLevel,
                    role,
                    featureToTest);

                if (hasAccess)
                {
                    return true;
                }
            }

            return false;
        }
    }
}

